﻿/*
 * Copyright 2014 Francois Karman
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace Quartz.WebManager.Model
{
    /// <summary>
    /// Specifies that a data field value should be lower or equal to an other data field value.
    /// </summary>
    public class LowerOrEqualToAttribute : ValidationAttribute
    {
        /// <summary>
        /// Initializes a new instance of the LowerOrEqualToAttribute class.
        /// </summary>
        public LowerOrEqualToAttribute()
        { }

        /// <summary>
        /// Initializes a new instance of the LowerOrEqualToAttribute class for a specified property.
        /// </summary>
        /// <param name="linkedProperty">The name of the linked property.</param>
        public LowerOrEqualToAttribute(string linkedProperty)
        {
            this.LinkedProperty = linkedProperty;
        }

        /// <summary>
        /// Gets or sets the name of the linked property.
        /// </summary>
        public string LinkedProperty { get; set; }

        /// <summary>
        /// Gets a value that indicates whether the attribute requires validation context. 
        /// </summary>
        /// <value>
        /// Returns always <c>true</c>.
        /// </value>
        public override bool RequiresValidationContext
        {
            get { return true; }
        }

        /// <summary>
        /// Determines whether the specified value is lower or equal to the linked value.
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="ValidationResult"/> class.
        /// </returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (string.IsNullOrEmpty(this.LinkedProperty))
            {
                return new ValidationResult("The LowerOrEqual check can't be performed - the linked property is not defined");
            }
            else if (validationContext == null)
            {
                throw new ArgumentNullException("validationContext", "This implementation requires a validation context");
            }

            // Checks if the value is empty
            if (value == null)
            {
                return ValidationResult.Success;
            }

            PropertyInfo property = validationContext.ObjectType.GetProperty(this.LinkedProperty);
            if (property == null)
            {
                string format = "The LowerOrEqual check is associated to an unknown property ({0})";
                string message = string.Format(format, this.LinkedProperty);
                throw new Exception(message);
            }
            else if (!property.CanRead)
            {
                string format = "The LowerOrEqual check is associated to a property that can't be read ({0}) - checks the protection level of the property";
                string message = string.Format(format, this.LinkedProperty);
                throw new Exception(message);
            }
            else if (!typeof(IComparable).IsAssignableFrom(property.PropertyType)
                && !typeof(IComparable).IsAssignableFrom(Nullable.GetUnderlyingType(property.PropertyType)))
            {
                string format = "The LowerOrEqual check is associated to a type that can't be compared ({0}) - the type of the property should implements IComparable";
                string message = string.Format(format, this.LinkedProperty);
                throw new Exception(message);
            }

            IComparable linkedValue = RetrieveComparable(property.GetValue(validationContext.ObjectInstance));
            IComparable currentValue = RetrieveComparable(value);

            if (linkedValue == null || currentValue == null)
            {
                return ValidationResult.Success;
            }
            else if (currentValue.CompareTo(linkedValue) <= 0)
            {
                return ValidationResult.Success;
            }
            else
            {
                return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName), new[] { validationContext.MemberName });
            }
        }

        /// <summary>
        /// Applies formatting to an error message, based on the data field where the error occurred. 
        /// </summary>
        /// <param name="name">The name to include in the formatted message.</param>
        /// <returns>An instance of the formatted error message.</returns>
        public override string FormatErrorMessage(string name)
        {
            // TODO: confirm that the error message remains overridable
            return string.Format("The field {0} should be lower or equal than its reference", name);
        }

        private IComparable RetrieveComparable(object value)
        {
            if (value == null)
            {
                return null;
            }

            IComparable cast = value as IComparable;
            if (cast != null)
            {
                return cast;
            }

            if (!typeof(IComparable).IsAssignableFrom(Nullable.GetUnderlyingType(value.GetType())))
            {
                throw new ArgumentException("The parameter should implements IComparable", "value");
            }
            else
            {
                // value is Nullable
                return value.GetType().GetProperty("Value").GetValue(value) as IComparable;
            }
        }
    }
}
